Skip to content

fix(spec): give every filter operator member a .describe() so the published filter reference documents all of them - #16432

Merged
huangyiirene merged 2 commits into
mainfrom
claude/issue-15059-filter-operator-describe
Sep 7, 2026
Merged

fix(spec): give every filter operator member a .describe() so the published filter reference documents all of them#16432
huangyiirene merged 2 commits into
mainfrom
claude/issue-15059-filter-operator-describe

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #15059

content/docs/references/data/filter.mdx fills its Description column from prop.description — the JSON-Schema projection of a Zod .describe() (packages/spec/scripts/lib/schema-section.ts). A JSDoc block above a member never reaches that column. Every filter-operator member now carries a .describe(), so the published reference documents all of them instead of a subset.

Which path, and why

Path 1 — every operator member gets a .describe(). The card left the choice to this seat; the measurement made path 1 the cheap option rather than the expensive one.

Walking the live Zod graph (OS_EAGER_SCHEMAS=1, shapes read off the built schemas, not grepped) over the twelve schemas filter.zod.ts exports: 39 member slots, 20 described, 19 blank at the merge base. But those 19 do not need 19 new sentences. Seven of them are the page-facing blanks and need real prose; the other twelve are FieldOperatorsSchema restating operators whose prose already exists verbatim in the documentation copy. Hoisting that text into module constants and reading it from both copies covers those twelve mechanically.

So path 1 cost seven new descriptions plus a de-duplication — and a narrow pass would have re-created the very deferral this card exists to end (#14048 pushed exactly this sweep to "another card"; this is that card). After: 39 of 39 members described, 0 blank.

The duplicated names, all of them

The card warned that the same operator names appear more than once. They do — every one of the eighteen operator names is declared exactly twice, in the documentation copy and in the enforced FieldOperatorsSchema, and both sites are now described:

operator documentation copy enforced copy (FieldOperatorsSchema)
$eq $ne EqualityOperatorSchema :252 :255 :1281 :1282
$gt $gte $lt $lte ComparisonOperatorSchema :409 :412 :415 :418 :1291 :1292 :1293 :1294
$in $nin SetOperatorSchema :559 :562 :1300 :1301
$between RangeOperatorSchema :716 :1314
$contains $notContains $startsWith $endsWith $icontains $like $ilike StringOperatorSchema :912 :915 :918 :921 :927 :935 :941 :1319 :1320 :1321 :1322 :1323 :1327 :1328
$null $exists SpecialOperatorSchema :1258 :1265 :1331 :1332

Rather than describing each pair twice, each description is now one module-level constant read by both sites — extending the pairing ORDERING_COMPARAND_DESCRIPTION and SET_MEMBER_DESCRIPTION already gave the ordering and set slots, whose own comment states the reason: the copies "share the CODE rather than a description of it, so they cannot drift". The remaining families now do the same, so an operator can no longer be documented in one copy and blank in the other.

$exists says the field HAS A VALUE

Its text is unchanged and still opens "Has-a-value check — the exact inverse of $null". $ne and $eq now say the same thing from their side ("{ "$ne": null } is the HAS-A-VALUE predicate ... it matches rows whose field holds a value, never rows that merely carry the key"), and the constant carries a comment saying a rewrite must keep saying "has a value". No key-presence wording is introduced anywhere.

The corpus ratchet reads identically

Both named rows unchanged before and after regeneration. The whole verdict block is byte-identical (diff of the two runs is empty):

BEFORE (merge base 0a038cc06d, before any edit)         AFTER (regenerated, final head 813450c8b5)
check-corpus-claim-drift: OK, no new claim sites        check-corpus-claim-drift: OK, no new claim sites
  Rules: 4 row(s) — exists-key-presence 4,                Rules: 4 row(s) — exists-key-presence 4,
         exists-portability 0, regex-retired 0,                  exists-portability 0, regex-retired 0,
         section-visiblewhen-unbound 0.                          section-visiblewhen-unbound 0.
  Scanned: 236 file(s), content/docs 190, skills 46.      Scanned: 236 file(s), content/docs 190, skills 46.

Both exit 0. The mechanism is structural rather than lucky: this gate excludes content/docs/references by path (SKIP_SUBTREES), because a finding in a generated tree names the wrong file — so the page this PR regenerates is not part of the scanned population at all. The .describe() text lands in exactly two places, packages/spec/json-schema/ (gitignored) and content/docs/references/data/filter.mdx; skills/** carries none of it.

Clause 2 — confirmed mechanically, not inherited

patch. No accept-set movement, no new export, no new key. Confirmed by running the gates rather than by reading the diff:

  • pnpm --filter @objectstack/spec check:api-surface — exit 0
  • pnpm --filter @objectstack/spec check:authorable-surface — exit 0
  • pnpm --filter @objectstack/spec check:generated — exit 0 after regeneration; before it, its only stale artifact was content/docs/references/**, with api-surface/, authorable-surface/ + authorable-defaults/ + the .base.json anchor, export-origins/ and declaration-map/ all reported current on the same run. A rebuild of @objectstack/spec left git status clean apart from the page, so .describe() moved no committed projection of the schema graph.

gen:docs regenerated 228 files and modified one.

Counts, re-taken on this tree

Re-counted here rather than quoted, as the card instructs, each with a control that proves the search could return a hit.

The describe( figure drifts because two different things are being counted. At the merge base, grep -c 'describe(' answers 17 and grep -c '\.describe(' answers 16; the difference is one prose occurrence, a comment at :510 that spells the word describe() inside a sentence about $in / $nin, and is not a call site. Neither number is what the defect depends on. (At the final head: 49 and 35.)

The defect itself, with its control. At the merge base $eq carried the JSDoc /** Equal to (default) - SQL: = | MongoDB: $eq */ at :224, and on the generated page of the same commit:

grep -c 'Equal to (default)' content/docs/references/data/filter.mdx   -> 0     the JSDoc does not reach the page
grep -c '**$eq**'            content/docs/references/data/filter.mdx   -> 1     control: the row rendered, the pattern can hit

Blank Description cells on the page. The same regex over the same file, at the merge base and at the final head:

merge base -> 7   ($eq, $ne, where, $contains, $notContains, $startsWith, $endsWith)
final head -> 0

The 7 is the control for the 0: one pattern, one file, two commits.

One sub-claim of the card is not correct, and it is filed rather than fixed here

The card says FieldOperatorsSchema's restated $null / $exists leave "the FieldOperator table on the same generated page" blank. There is no FieldOperator table on that page. build-schemas.ts skips four of filter.zod.ts's exports outright, from its own output on this tree:

⊘ Data.ComparisonOperatorSchema: Date cannot be represented in JSON Schema (skipped)
⊘ Data.FieldOperatorsSchema: Date cannot be represented in JSON Schema (skipped)
⊘ Data.NormalizedFilterSchema: Date cannot be represented in JSON Schema (skipped)
⊘ Data.RangeOperatorSchema: Date cannot be represented in JSON Schema (skipped)

so grep -c 'FieldOperator' on the page returns 0 against a control of SpecialOperator returning 2. The card's core premise is unaffected and holds: JSDoc does not reach the page, .describe() does, and seven cells on a published page were blank.

A consequence of that skip is a real gap this PR deliberately does not touch — $gt / $gte / $lt / $lte / $between reach no published reference row at all, and the remedy is a generator or schema-shape change, which this card forbids. Filed as #16431. The members are described here anyway, so the two copies cannot drift.

Verification

command exit
pnpm --filter @objectstack/spec build 0
pnpm --filter @objectstack/spec typecheck (incl. check:test-typecheck) 0
pnpm --filter @objectstack/spec test — 482 files, 13102 tests, all passed 0
pnpm --filter @objectstack/spec check:generated (15 artifacts) 0
pnpm check:corpus-claim-drift (incl. --self-test) 0
pnpm check:nul-bytes 0
the other 84 gates derived by scripts/pm/dispatch-gates.mjs --commands for this changeset all 0

dispatch-gates.mjs was re-derived after the changeset was written (85 commands before it, 91 after). Four gates first answered PREREQUISITE NOT MET because a package was unbuilt, not because of this diff; after building @objectstack/formula, @objectstack/lint, @objectstack/client-react and @objectstack/client all four pass: check:doc-formula-expressions, check:doc-security-posture, check:skill-examples, check:docs-transcript-drift.

Declared narrowing. pnpm check:dual-build-cjs-loads remains NOT MEASURED locally (exit 3, PREREQUISITE NOT MET): it reads every package's dist/, so it needs a full pnpm build, and it is left to CI. turbo ls --affected names 75 packages — every consumer of @objectstack/spec — so the affected-package test sweep is CI's run rather than this container's; no test anywhere asserts on these description strings (git grep for the existing description text across *.ts / *.mts / *.mjs returns only filter.zod.ts itself, against a control of FieldOperatorsSchema returning 10 files, test files among them).

Gate exits were captured before any pipe (cmd > log 2>&1; EXIT=$?), and the ratchet figures above are quoted from the gates' own verdict lines.

#15778 is not addressed here — same file, different pipeline, its own PR and changeset, exactly as triage ruled.


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

20 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 130 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3e7ef9c2389e3e5e02903634947de9cece4736b4packageMentionDocs.

Which tree this was computed on

This run read content/docs from d3e93433ee4d320cfcdbaf63828ac4f2bc75eb7b — the merge of head 813450c8b5fea43058d3749979a3e9f2b9df3183 into base 3e7ef9c2389e3e5e02903634947de9cece4736b4, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d3e93433ee4d320cfcdbaf63828ac4f2bc75eb7b && git checkout d3e93433ee4d320cfcdbaf63828ac4f2bc75eb7b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3e7ef9c2389e3e5e02903634947de9cece4736b4 813450c8b5fea43058d3749979a3e9f2b9df3183 && git checkout -B drift-repro 3e7ef9c2389e3e5e02903634947de9cece4736b4 && git merge --no-ff 813450c8b5fea43058d3749979a3e9f2b9df3183

node scripts/docs-audit/affected-docs.mjs --json 3e7ef9c2389e3e5e02903634947de9cece4736b4

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Copy link
Copy Markdown
Collaborator Author

PM — ready, auto-merge armed. Answering the open question, and recording two things the dev got right that were not what the dispatch asked for.

The open question: attribution footer form — A, follow AGENTS.md

You chose correctly and were right to flag it rather than pick silently. For an artifact that lives in this repository and is read by this repository's gates and reviewers, AGENTS.md is the authority, and its form is backed by an in-repo measurement that create_pull_request keeps it verbatim — which your read-back confirmed (block survived byte-identical, no second footer appended). A harness-side convention governs what the harness itself writes; it does not override the repo's own contract for repo artifacts. No change needed.

You overturned my recommendation's reasoning while agreeing with its conclusion, and that is the more useful outcome

I recommended Path 1 on provenance grounds — this card exists because #14048 deferred the sweep, so a narrow pass sets up the next deferral. I explicitly said I had not measured the member population and you would.

You did, and the economics are the reverse of what I implied: Path 1 was the cheap option. Only 7 of the 19 blanks needed new prose; the other 12 are FieldOperatorsSchema restating operators whose text already existed verbatim, so hoisting each description into a module-level constant read by both copies covered them mechanically. That is a better argument for Path 1 than mine was, and it means the "expensive vs narrow" framing I handed you was wrong on the facts.

The sub-claim you falsified is bigger than the card

there is no FieldOperator table on filter.mdx at all — build-schemas.ts skips ComparisonOperatorSchema, FieldOperatorsSchema, RangeOperatorSchema and NormalizedFilterSchema outright over an unrepresentable z.date(), so $gt / $gte / $lt / $lte / $between reach no published reference row at all — with a console.warn in a build that exits 0.

Correctly filed as #16431 and not folded in: the remedy is a generator or schema-shape change, which this card's scope forbids. A silent-warning build that drops five operators from the published reference is a larger defect than the blank cells this card set out to fill, and it would have stayed invisible if you had implemented the card as written.

Method worth naming

Clause ② no, confirmed mechanically rather than inherited (check:api-surface and check:authorable-surface both exit 0; content/docs/references/** the single stale artifact). patch is right.

If Test Core comes back red here, read Test Core (5/6)'s duration first — it is being killed at its 30-minute wall on a large share of runs today (#16173). Note this branch does carry the #16316 honesty fix (probe reads 1 against a control of 1 on main), so unlike some older branches its Test Core result is truthful in both directions.


Generated by Claude Code

@huangyiirene
huangyiirene added this pull request to the merge queue Sep 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 7, 2026
@huangyiirene
huangyiirene added this pull request to the merge queue Sep 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 7, 2026
@huangyiirene
huangyiirene added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit 74628d9 Sep 7, 2026
36 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-15059-filter-operator-describe branch September 7, 2026 04:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tooling

Projects

None yet

2 participants